a tool for shared writing and social publishing
at feature/reader 132 lines 4.7 kB view raw
1import { createIdentity } from "actions/createIdentity"; 2import { subscribeToPublication } from "app/lish/subscribeToPublication"; 3import { drizzle } from "drizzle-orm/node-postgres"; 4import { cookies } from "next/headers"; 5import { redirect } from "next/navigation"; 6import { NextRequest, NextResponse } from "next/server"; 7import { createOauthClient } from "src/atproto-oauth"; 8import { setAuthToken } from "src/auth"; 9 10import { supabaseServerClient } from "supabase/serverClient"; 11import { URLSearchParams } from "url"; 12import { 13 ActionAfterSignIn, 14 parseActionFromSearchParam, 15} from "./afterSignInActions"; 16import { pool } from "supabase/pool"; 17 18type OauthRequestClientState = { 19 redirect: string | null; 20 action: ActionAfterSignIn | null; 21}; 22 23export async function GET( 24 req: NextRequest, 25 props: { params: Promise<{ route: string; handle?: string }> }, 26) { 27 const params = await props.params; 28 let client = await createOauthClient(); 29 switch (params.route) { 30 case "metadata": 31 return NextResponse.json(client.clientMetadata); 32 case "jwks": 33 return NextResponse.json(client.jwks); 34 case "login": { 35 const searchParams = req.nextUrl.searchParams; 36 const handle = searchParams.get("handle") as string; 37 // Put originating page here! 38 let redirect = searchParams.get("redirect_url"); 39 if (redirect) redirect = decodeURIComponent(redirect); 40 let action = parseActionFromSearchParam(searchParams.get("action")); 41 let state: OauthRequestClientState = { redirect, action }; 42 43 // Revoke any pending authentication requests if the connection is closed (optional) 44 const ac = new AbortController(); 45 46 const url = await client.authorize(handle || "https://bsky.social", { 47 scope: "atproto transition:generic transition:email", 48 signal: ac.signal, 49 state: JSON.stringify(state), 50 }); 51 52 return NextResponse.redirect(url); 53 } 54 case "callback": { 55 const params = new URLSearchParams(req.url.split("?")[1]); 56 57 let redirectPath = "/"; 58 try { 59 const { session, state } = await client.callback(params); 60 let s: OauthRequestClientState = JSON.parse(state || "{}"); 61 redirectPath = decodeURIComponent(s.redirect || "/"); 62 let { data: identity } = await supabaseServerClient 63 .from("identities") 64 .select() 65 .eq("atp_did", session.did) 66 .single(); 67 if (!identity) { 68 let existingIdentity = (await cookies()).get("auth_token"); 69 if (existingIdentity) { 70 let data = await supabaseServerClient 71 .from("email_auth_tokens") 72 .select("*, identities(*)") 73 .eq("id", existingIdentity.value) 74 .single(); 75 if (data.data?.identity && data.data.confirmed) 76 await supabaseServerClient 77 .from("identities") 78 .update({ atp_did: session.did }) 79 .eq("id", data.data.identity); 80 81 return handleAction(s.action, redirectPath); 82 } 83 const client = await pool.connect(); 84 const db = drizzle(client); 85 identity = await createIdentity(db, { atp_did: session.did }); 86 client.release(); 87 } 88 let { data: token } = await supabaseServerClient 89 .from("email_auth_tokens") 90 .insert({ 91 identity: identity.id, 92 confirmed: true, 93 confirmation_code: "", 94 }) 95 .select() 96 .single(); 97 98 if (token) await setAuthToken(token.id); 99 100 // Process successful authentication here 101 console.log("authorize() was called with state:", state); 102 103 console.log("User authenticated as:", session.did); 104 return handleAction(s.action, redirectPath); 105 } catch (e) { 106 redirect(redirectPath); 107 } 108 } 109 default: 110 return NextResponse.json({ error: "Invalid route" }, { status: 404 }); 111 } 112} 113 114const handleAction = async ( 115 action: ActionAfterSignIn | null, 116 redirectPath: string, 117) => { 118 let parsePath = decodeURIComponent(redirectPath); 119 let url; 120 if (parsePath.includes("://")) url = new URL(parsePath); 121 else url = new URL(decodeURIComponent(redirectPath), "https://example.com"); 122 if (action?.action === "subscribe") { 123 let result = await subscribeToPublication(action.publication); 124 if (result.hasFeed === false) 125 url.searchParams.set("showSubscribeSuccess", "true"); 126 } 127 128 let path = url.pathname; 129 if (url.search) path += url.search; 130 if (url.hash) path += url.hash; 131 return parsePath.includes("://") ? redirect(url.toString()) : redirect(path); 132};